﻿
using System;
using System.Collections.Generic;
using System.Linq;


namespace Boilen.Primitives.Members {

    /// <summary>
    /// Writes a named block of code enclosed.
    /// </summary>
    public sealed class Guard : IWritable {

        /// <summary>
        /// An empty guard that can be used as a flag for including the validation sub-namespace.
        /// </summary>
        public static Guard Empty = new Guard( );

        private readonly bool isEmpty_;
        private readonly Doc doc_;
        private readonly string guardName_;
        private readonly string valueName_;
        private readonly Type exceptionType_;
        private readonly string description_;
        private readonly List<string> arguments_ = new List<string>( );


        /// <summary>
        /// Gets a value indicating whether this is the <see cref="Empty"/> guard.
        /// </summary>
        public bool IsEmpty { get { return this.isEmpty_; } }

        /// <summary>
        /// Gets or sets a value indicating whether this is a "Value" or "Param" guard.
        /// </summary>
        public bool IsValueGuard { get; set; }

        /// <summary>
        /// Gets or sets the priority of the guard.
        /// </summary>
        public int Priority { get; set; }

        /// <summary>
        /// Gets the name of the guard.
        /// </summary>
        public string GuardName { get { return this.guardName_; } }

        /// <summary>
        /// Gets the name of the value protected by the guard.
        /// </summary>
        public string ValueName { get { return this.valueName_; } }

        /// <summary>
        /// Gets the type of the exception thrown by the guard.
        /// </summary>
        public Type ExceptionType { get { return this.exceptionType_; } }

        /// <summary>
        /// Gets the documentation for the guard.
        /// </summary>
        public string Description { get { return this.description_ == null ? null : this.IsEmpty ? "" : this.doc_.ApplyReplacements( this.description_ ); } }

        /// <summary>
        /// Gets the list of arguments for the guard.
        /// </summary>
        public IList<string> Arguments { get { return this.arguments_; } }


        /// <summary>
        /// Initializes a new <see cref="Guard"/> instance.
        /// </summary>
        /// <param name="doc">The documentation for the parent member.</param>
        /// <param name="guardName">The name of the guard method.</param>
        /// <param name="valueName">The name of the parameter to guard.</param>
        /// <param name="exceptionType">The type of the exception thrown by the guard.</param>
        /// <param name="description">The summary documentation for the guard.</param>
        /// <param name="ensureDescription">A value indicating whether to verify <paramref name="description"/>.</param>
        public Guard( Doc doc, string guardName, string valueName, Type exceptionType, string description, bool ensureDescription = true ) {
            Ensure.NotNull( doc );
            Ensure.NotNullOrEmpty( guardName );
            Ensure.NotNullOrEmpty( valueName );
            Ensure.NotNull( exceptionType );
            if( ensureDescription ) {
                if( GlobalSettings.ExternalDocumentationPrefix != null && !string.IsNullOrEmpty( description ) )
                    throw new InvalidOperationException( guardName + " guard description '" + description + "' cannot be used when specifying external documentation." );
                if( GlobalSettings.ExternalDocumentationPrefix == null && string.IsNullOrEmpty( description ) )
                    throw new InvalidOperationException( guardName + " guard description must be specified when not using external documentation." );
            }

            this.isEmpty_ = false;
            this.doc_ = new Doc( doc ).AddExpander( Doc.Name, valueName );

            this.guardName_ = guardName;
            this.valueName_ = valueName;
            this.exceptionType_ = exceptionType;
            this.description_ = description;
        }

        private Guard( ) {
            this.isEmpty_ = true;
            this.doc_ = null;
            this.exceptionType_ = null;
            this.guardName_ = this.valueName_ = this.description_ = "";
        }


        /// <inheritdoc/>
        public void Write( ICodeWriter writer ) {
            string argumentsString = Util.Join( this.Arguments, ", " );
            writer.Write( ".{0}({1})", this.GuardName, argumentsString );
        }


        /// <inheritdoc/>
        public Guard SetPriority( int priority ) {
            this.Priority = priority;
            return this;
        }


        #region Standard Guards

        /// <summary>
        /// Creates a guard to ensure a value is not <null/>.
        /// </summary>
        public static Guard NotNull( Doc doc, string valueName, bool forceDescription ) {
            return new Guard( doc, "NotNull", valueName, typeof( ArgumentNullException ), forceDescription || GlobalSettings.ExternalDocumentationPrefix == null ? "%paramref:name% is null." : null, !forceDescription );
        }

        /// <summary>
        /// Creates a guard to ensure an <see cref="Enum"/> value is defined.
        /// </summary>
        public static Guard EnumIsDefined( Doc doc, string valueName, bool forceDescription ) {
            return new Guard( doc, "EnumIsDefined", valueName, typeof( ArgumentException ), forceDescription || GlobalSettings.ExternalDocumentationPrefix == null ? "%paramref:name% is not a valid %see:type% value." : null, !forceDescription );
        }

        /// <summary>
        /// Creates a guard to ensure an <see cref="double"/> value is not a special value.
        /// </summary>
        public static Guard NotSpecialValue( Doc doc, string valueName, bool forceDescription ) {
            return new Guard( doc, "NotSpecialValue", valueName, typeof( ArgumentOutOfRangeException ), forceDescription || GlobalSettings.ExternalDocumentationPrefix == null ? "%paramref:name% is %see:double.NaN%, %see:double.NegativeInfinity%, or %see:double.PositiveInfinity%." : null, !forceDescription );
        }

        /// <summary>
        /// Creates a guard to ensure a value is within the range defined by the specified condition.
        /// </summary>
        public static Guard IsInRange( Doc doc, string valueName, string description, string condition, string format, params string[] args ) {
            return Guard.AddArguments(
                new Guard( doc, "IsInRange", valueName, typeof( ArgumentOutOfRangeException ), description ),
                condition, format, args
            );
        }

        /// <summary>
        /// Creates a guard to ensure a value satisfies the requirements defined by the specified condition.
        /// </summary>
        public static Guard Satisfies( Doc doc, string valueName, string description, string condition, string format, params string[] args ) {
            return Guard.AddArguments(
                new Guard( doc, "Satisfies", valueName, typeof( ArgumentException ), description ),
                condition, format, args
            );
        }

        /// <summary>
        /// Creates a guard to ensure an accessor satisfies the requirements defined by the specified condition.
        /// </summary>
        public static Guard AccessorSatisfies( Doc doc, string valueName, string description, string condition, string format, params string[] args ) {
            return Guard.AddArguments(
                new Guard( doc, "Satisfies", valueName, typeof( InvalidOperationException ), description ) { IsValueGuard = true, Priority = 1 },
                condition, format, args
            );
        }


        private static Guard AddArguments( Guard guard, string condition, string format, IEnumerable<string> remainingArgs ) {
            Ensure.NotNull( guard, remainingArgs );
            Ensure.NotNullOrEmpty( condition );
            Ensure.NotNullOrEmpty( format );

            guard.Arguments.Add( condition );
            guard.Arguments.Add( '"' + format + '"' );
            foreach( string arg in remainingArgs )
                guard.Arguments.Add( arg );

            return guard;
        }

        #endregion

        /// <summary>
        /// Writes each guard in the specified collection from highest to lowest priority.
        /// </summary>
        public static void WriteGuards( ICodeWriter writer, IEnumerable<Guard> guards ) {
            Ensure.NotNull( writer, guards );

            var guardsByKind = guards
                .Where( g => !g.IsEmpty )
                .OrderByDescending( g => g.Priority )
                .GroupBy( g => g.IsValueGuard ? "Value" : "Param" )
                .ToArray( );

            foreach( var kind in guardsByKind ) {
                string guardKind = kind.Key;
                var targetedGuards = kind.GroupBy( g => g.ValueName );

                foreach( var group in targetedGuards ) {
                    string guardTarget = group.Key;

                    writer.WriteLine( "{0}.Guard{1}(\"{0}\")", guardTarget, guardKind );
                    using( Enclose.Indent( writer ) ) {
                        Util.Iterate( group, ( i, last ) => writer.WriteLine( ), ( i, guard ) => guard.Write( writer ) );
                        writer.WriteLine( ";" );
                    }
                }
            }

            if( guardsByKind.Any( ) )
                writer.WriteLine( );
        }

        /// <summary>
        /// Returns a new collection of guards with <see cref="Guard.ValueName"/> set to the specified value.
        /// </summary>
        public static Guard[] ChangeValueName( IEnumerable<Guard> guards, string valueName ) {
            Ensure.NotNull( guards );
            Ensure.NotNullOrEmpty( valueName );

            return guards.Select( guard =>
                new Guard( guard.doc_, guard.guardName_, valueName, guard.exceptionType_, guard.description_, false ) {
                    Priority = guard.Priority
                }.SetArguments( guard.Arguments )
            ).ToArray( );
        }


        private Guard SetArguments( IEnumerable<string> arguments ) {
            this.arguments_.AddRange( arguments );
            return this;
        }

    }

}
